/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.context.scope.refresh;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import lombok.Data;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.cloud.context.scope.GenericScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

/**
 * <p>
 * A Scope implementation that allows for beans to be refreshed dynamically at runtime
 * (see {@link #refresh(String)} and {@link #refreshAll()}). If a bean is refreshed then
 * the next time the bean is accessed (i.e. a method is executed) a new instance is
 * created. All lifecycle methods are applied to the bean instances, so any destruction
 * callbacks that were registered in the bean factory are called when it is refreshed, and
 * then the initialization callbacks are invoked as normal when the new instance is
 * created. A new bean instance is created from the original bean definition, so any
 * externalized content (property placeholders or expressions in string literals) is
 * re-evaluated when it is created.
 * </p>
 *
 * <p>
 * Note that all beans in this scope are <em>only</em> initialized when first accessed, so
 * the scope forces lazy initialization semantics.
 * </p>
 *
 * <p>
 * The scoped proxy approach adopted here has a side benefit that bean instances are
 * automatically {@link Serializable}, and can be sent across the wire as long as the
 * receiver has an identical application context on the other side. To ensure that the two
 * contexts agree that they are identical, they have to have the same serialization ID.
 * One will be generated automatically by default from the bean names, so two contexts
 * with the same bean names are by default able to exchange beans by name. If you need to
 * override the default ID, then provide an explicit {@link #setId(String) id} when the
 * Scope is declared.
 * </p>
 *
 * @author Dave Syer
 * @since 3.1
 */
@ManagedResource
@Data
public class RefreshScope
        extends GenericScope
        implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {

    private ApplicationContext context;

    private BeanDefinitionRegistry registry;

    private boolean eager = true;

    private int order = Ordered.LOWEST_PRECEDENCE - 100;

    public RefreshScope() {
        super.setName("refresh");
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        this.registry = registry;
        super.postProcessBeanDefinitionRegistry(registry);
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        start(event);
    }

    public void start(ContextRefreshedEvent event) {
        if (event.getApplicationContext() == this.context && this.eager && this.registry != null) {
            eagerlyInitialize();
        }
    }

    private void eagerlyInitialize() {
        String[] names = this.context.getBeanDefinitionNames();
        List<Map.Entry<String, BeanDefinition>> collect = Arrays.stream(names)
                .map(name -> Map.entry(name, this.registry.getBeanDefinition(name)))
                .filter(entry -> entry.getValue().getScope().equals(this.getName()) && !entry.getValue().isLazyInit())
                .collect(Collectors.toUnmodifiableList());
        for (Map.Entry<String, BeanDefinition> entry : collect) {
            Object bean = this.context.getBean(entry.getKey());
            if (bean != null) {
                bean.getClass();
            }
        }
//        for (String name : names) {
//            BeanDefinition definition = this.registry.getBeanDefinition(name);
//            if (this.getName().equals(definition.getScope()) && !definition.isLazyInit()) {
//                Object bean = this.context.getBean(name);
//                if (bean != null) {
//                    bean.getClass();
//                }
//            }
//        }
    }

    public boolean refresh(Class type) {
        String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.context, type);
        if (beanNamesForType.length > 0) {
            return refresh(beanNamesForType[0]);
        }
        return false;
    }

    @ManagedOperation(description = "Dispose of the current instance of bean name " + "provided and force a refresh on next method execution.")
    public boolean refresh(String name) {
        if (!ScopedProxyUtils.isScopedTarget(name)) {
            name = ScopedProxyUtils.getTargetBeanName(name);
        }
        if (super.destroy(name)) {
            this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
            return true;
        }
        return false;
    }

    @ManagedOperation(description = "Dispose of the current instance of all beans " + "in this scope and force a refresh on next method execution.")
    public void refreshAll() {
        super.destroy();
        this.context.publishEvent(new RefreshScopeRefreshedEvent());
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }
}
